package org.example.filter;

import cn.lingyangwl.framework.cache.redis.utils.RedisUtils;
import cn.lingyangwl.framework.core.response.Resp;
import cn.lingyangwl.framework.core.utils.servlet.ServletUtils;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import lombok.SneakyThrows;
import org.example.annotation.ApiSecurity;
import org.example.enums.SecurityTypeEnum;
import org.example.handler.RequestApiSafe;
import org.example.utils.JwtUtils;
import org.example.utils.RsaHelper;
import org.example.utils.SignHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerMapping;

import javax.servlet.FilterChain;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.*;

/**
 * Description: 请求参数解密过滤器 / 签名<br>
 * 需要解密的方法，在对应方法或者类上添加注解@CryptoType(CryptoType.CRYPTO)<br>
 * 前端加密会把参数通过AES方式加密成base64，“encrypt”： 加密后内容<br>
 * FIXME 当前只做请求参数加密，返回值未做加密
 *
 * @author shenguangyang
 */
public class RequestApiSafeFilter extends OncePerRequestFilter implements ApplicationContextAware {
    private static final Logger log = LoggerFactory.getLogger(RequestApiSafeFilter.class);

    @Autowired(required = false)
    RequestApiSafe requestApiSafe;

    /**
     * 用于加密参与签名加密的key
     */
    @Value("${sign.rsa.privateKey}")
    private String signKeyRsaPrivateKey;

    /** 方法映射集 */
    private List<HandlerMapping> handlerMappings;

    private static final String APPLICATION_JSON = "application/json";

    /**
     * 时间戳超时时间以及签名存在缓存中的存活时间
     */
    private static final long EXPIRE_TIME = 60;

    /** RSA加解密 */
    @Value("${sign.rsa.privateKey}")
    private String rsaPrivateKey;

    @SuppressWarnings("NullableProblems")
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext,
                HandlerMapping.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerMappings = new ArrayList<>(matchingBeans.values());
            // We keep HandlerMappings in sorted order.
            AnnotationAwareOrderComparator.sort(this.handlerMappings);
        }
    }

    @SuppressWarnings("NullableProblems")
    @SneakyThrows
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) {
        try {
            String m1 = request.getMethod();
            if (HttpMethod.OPTIONS.toString().equals(m1)){
                response.setStatus(HttpStatus.NO_CONTENT.value());
                filterChain.doFilter(request, response);
                return;
            }

            Object handler = getHandler(request).getHandler();
            if (handler instanceof HandlerMethod) {
                // 方法上是否有注解
                HandlerMethod method = (HandlerMethod) handler;
                ApiSecurity type = method.getMethodAnnotation(ApiSecurity.class);
                if (null == type) {
                    // 类上是否有注解
                    type = method.getBeanType().getAnnotation(ApiSecurity.class);
                    if (null == type) {
                        filterChain.doFilter(request, response);
                        return;
                    }
                }

                // 获取令牌，签名，以及时间戳
                String token = request.getHeader("token");
                String ts = request.getHeader("ts");
                // 随机数，前端生成随机数主要保证签名的多变性
                String nonce = request.getHeader("nonce");
                String sign = request.getHeader("sign");

                // 校验token是否合法
                Optional<RequestApiSafe> optionalRequestApiSafe = Optional.ofNullable(this.requestApiSafe);
                RequestApiSafe requestApiSafe = optionalRequestApiSafe.orElseGet(() ->
                        new RequestApiSafe() {
                            @Override
                            public boolean verifyToken(String token) {
                                return JwtUtils.isVerify(token);
                            }
                        }
                );
                if (StringUtils.isEmpty(token) || !requestApiSafe.verifyToken(token)) {
                    request.setAttribute("code",1005);
                    request.setAttribute("msg","令牌失效，请重新登录!!!");
                    filterChain.doFilter(request, response);
                    return;
                }

                Resp<?> responseResult = requestApiSafe.afterVerifyTokenSuccess();
                if (responseResult.getCode() != 200) {
                    request.setAttribute("code",responseResult.getCode());
                    request.setAttribute("msg",responseResult.getMsg());
                    filterChain.doFilter(request, response);
                    return;
                }
                // 判断是否是解密
                // 如果是加密，则请求头中不需要携带nonce,ts , sign等字段
                if (type.value() == SecurityTypeEnum.CRYPTO) {
                    // 不是解密跳过
                    filterChain.doFilter(new DecryptRequest(request), response);
                    return;
                }

                // 判断需要的参数是否为空，不为空进行后续操作
                if (StringUtils.isEmpty(sign) || StringUtils.isEmpty(ts) || StringUtils.isEmpty(nonce)){
                    request.setAttribute("code",1001);
                    request.setAttribute("msg","非法参数: sign,ts,nonc不能非空!!!");
                    filterChain.doFilter(request, response);
                    return;
                }

                // 判断是否需要验证签名
                if (type.value() == SecurityTypeEnum.SIGN) {
                    // 防止流读取一次后就没有了, 所以需要将流继续写出去
                    HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
                    // 是签名
                    // 获取全部参数(包括URL和body上的)
                    Map<String, Object> allParams = ServletUtils.getAllParams(requestWrapper);
                    SignRequest signRequest = new SignRequest();
                    // 签名验证失败会往请求中写入数据
                    signRequest.verifySign(requestWrapper,allParams);
                    filterChain.doFilter(requestWrapper, response);
                    return;
                }
            }
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            log.error(e.getMessage());
            request.setAttribute("code",-1);
            request.setAttribute("msg",e.getMessage());
            filterChain.doFilter(request, response);
        }
    }


    /**
     * 获取访问目标方法
     *
     * @param request
     * @return HandlerExecutionChain
     * @throws Exception
     */
    protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        if (this.handlerMappings != null) {
            for (HandlerMapping hm : this.handlerMappings) {
                if (log.isTraceEnabled()) {
                    log.trace("Testing handler map [" + hm + "] in DispatcherServlet with name ''");
                }
                HandlerExecutionChain handler = hm.getHandler(request);
                if (handler != null) {
                    return handler;
                }
            }
        }
        return null;
    }

    /**
     * 签名请求
     */
    private class SignRequest {
        /**
         * 校验签名是否一致
         */
        public void verifySign(HttpServletRequest request , Map<String,Object> params) {
            String ts = request.getHeader("ts");
            String sign = request.getHeader("sign");
            // key由前端传过来的加密字符串，通过rsa进行加密
            String key = request.getHeader("key");

            try {
                // 判断是否重复访问 (重放攻击)，存在重放攻击的时间窗口期 ，只能减轻重放攻击
                if (SignHelper.getTimestamp() - Long.parseLong(ts) > EXPIRE_TIME){
                    request.setAttribute("code",500);
                    request.setAttribute("msg","连接服务器超时!!!");
                    return;
                }

                // [采用 sign存入缓存 + 重放攻击的时间窗口期]防止重放攻击
                if (isRepeatedSubmit(sign)) {
                    request.setAttribute("code",500);
                    request.setAttribute("msg","请不要重复提交!!!");
                    return;
                }

                try {
                    key = RsaHelper.decrypt(key, signKeyRsaPrivateKey);
                } catch (Exception e) {
                    request.setAttribute("code", 500);
                    request.setAttribute("msg", "参数校验失败");
                    return;
                }

                // 验证签名
                if (!SignHelper.checkSign(params , request , sign , key)){
                    request.setAttribute("code",500);
                    request.setAttribute("msg","无效的签名");
                }
            } catch (Exception e) {
                request.setAttribute("code",500);
                request.setAttribute("msg",e.getMessage());
            }

        }

        /**
         * 判断是否是重复提交
         * 每次请求进来，根据key查询redis，如果存在则说明是重复提交，给予结果，如果不存在，则是正常提交，将key存入redis。
         * @param sign 签名
         */
        protected boolean isRepeatedSubmit(String sign) throws Exception {
            String key = "sign:" + sign;
            Object obj = RedisUtils.StringOps.get(key);
            if (obj == null) {
                RedisUtils.StringOps.setEx(key , "" , RequestApiSafeFilter.EXPIRE_TIME);
                return false;
            }
            return true;
        }
    }

    /**
     * 解密request封装
     *
     * @author ldy
     *
     */
    private class DecryptRequest extends HttpServletRequestWrapper {

        private static final String APPLICATION_JSON = "application/json";
        /** 所有参数的Map集合 */
        private Map<String, String[]> parameterMap;
        /** 输入流 */
        private InputStream inputStream;

        public DecryptRequest(HttpServletRequest request) throws IOException {
            super(request);
            String contentType = request.getHeader("Content-Type");
            log.debug("DecryptRequest -> contentType:{}", contentType);
            String encrypt = null;
            if (null != contentType && contentType.contains(APPLICATION_JSON)) {
                // json
                ServletInputStream io = request.getInputStream();
                ByteArrayOutputStream os = new ByteArrayOutputStream();
                byte[] buffer = new byte[1024];
                int length;
                while ((length = io.read(buffer)) != -1) {
                    os.write(buffer, 0, length);
                }
                byte[] bytes = os.toByteArray();
                encrypt = (String) JSON.parseObject(new String(bytes)).get("encrypt");
            } else {
                // url
                encrypt = request.getParameter("encrypt");
            }
            log.debug("DecryptRequest -> encrypt:{}", encrypt);
            // 解密
            String params = decrypt(encrypt);
            // 解密失败
            if (StringUtils.isEmpty(params)) {
                request.setAttribute("code",1009);
                request.setAttribute("msg","服务器拒绝请求");
                return;
            }
            if (null != contentType && contentType.contains(APPLICATION_JSON)) {
                if (this.inputStream == null) {
                    this.inputStream = new DecryptInputStream(new ByteArrayInputStream(params.getBytes()));
                }
            }
            parameterMap = buildParams(params);
        }

        private String decrypt(String encrypt) throws IOException {
            try {
                // 解密
                return RsaHelper.decrypt(encrypt,rsaPrivateKey);
            } catch (Exception e) {
                return null;
            }
        }

        private Map<String, String[]> buildParams(String src) throws UnsupportedEncodingException {
            Map<String, String[]> map = new HashMap<>(16);
            Map<String, String> params = JSONObject.parseObject(src, Map.class);
            for (String key : params.keySet()) {
                map.put(key, new String[] { params.get(key) });
            }
            return map;
        }

        @Override
        public String getParameter(String name) {
            String[] values = getParameterMap().get(name);
            if (values != null) {
                return (values.length > 0 ? values[0] : null);
            }
            return super.getParameter(name);
        }

        @Override
        public String[] getParameterValues(String name) {
            String[] values = getParameterMap().get(name);
            if (values != null) {
                return values;
            }
            return super.getParameterValues(name);
        }

        @Override
        public Enumeration<String> getParameterNames() {
            Map<String, String[]> multipartParameters = getParameterMap();
            if (multipartParameters.isEmpty()) {
                return super.getParameterNames();
            }

            Set<String> paramNames = new LinkedHashSet<String>();
            Enumeration<String> paramEnum = super.getParameterNames();
            while (paramEnum.hasMoreElements()) {
                paramNames.add(paramEnum.nextElement());
            }
            paramNames.addAll(multipartParameters.keySet());
            return Collections.enumeration(paramNames);
        }

        @Override
        public Map<String, String[]> getParameterMap() {
            return null == parameterMap ? super.getParameterMap() : parameterMap;
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            return this.inputStream == null ? super.getInputStream() : (ServletInputStream) this.inputStream;
        }
    }

    /**
     * 自定义ServletInputStream
     *
     * @author ldy
     *
     */
    private static class DecryptInputStream extends ServletInputStream {

        private final InputStream sourceStream;

        /**
         * Create a DelegatingServletInputStream for the given source stream.
         *
         * @param sourceStream
         *            the source stream (never {@code null})
         */
        public DecryptInputStream(InputStream sourceStream) {
            Assert.notNull(sourceStream, "Source InputStream must not be null");
            this.sourceStream = sourceStream;
        }

        @Override
        public int read() throws IOException {
            return this.sourceStream.read();
        }

        @Override
        public void close() throws IOException {
            super.close();
            this.sourceStream.close();
        }

        @Override
        public boolean isFinished() {
            return false;
        }

        @Override
        public boolean isReady() {
            return false;
        }

        @Override
        public void setReadListener(ReadListener readListener) {

        }
    }
}
